共计 1720 个字符,预计需要花费 5 分钟才能阅读完成。
提醒:本文最后更新于 2024-08-29 09:58,文中所关联的信息可能已发生改变,请知悉!
栈溢出原理
由于 C 语言对数组引用不做任何边界检查从而导致缓冲区溢出(buffer overflow)成为一种很常见的漏洞。根据溢出发生的内存位置通常可以分为栈溢出和堆溢出°其中’由于栈上保存着局部变量和一些状态信息(寄存器值、返回地址等),一旦发生严重的溢出, 攻击者就可以通过覆写返回地址来执行任意代码利用方法包括 shellcode 注入、ret2libc、ROP 等。同时防守方也发展出多种利用缓解机制。
函数调用栈
下面直接举例说明(分为 x86 和 x86-64)
x86
首先被调用函数
func()
的 8 个参数从后问前依次入栈,当执行call
指令时,下一条指令的地址0x08048415
作为返回地址入栈。然后程序跳转到func()
,在函数开头,将调用函数的ebp
压栈保存并更新为当前的栈顶地址esp
,作为新的栈基址,而esp
则下移为局部变量开辟空间。函数返回时则相反,通过leave
指令将esp
恢复为当前的ebp
,并从栈中将调用者的ebp
弹出,最后ret
指令弹出返回地址作为eip
,程序回到main()
函数中,最后抬高esp
清理被调用者的参数,一次函数调用的过程就结束了。
x86-64
对于 x86-64 的程序,前 6 个参数分别通过
rdi
、rsi
、rdx
、rcx
、r8
和r9
进行传递,剩余参数才像 x86 一样从后向前依次压栈。除此之外,我们还发现func()
没有下移rsp
开辟栈空间的操作,导致rbp
和rsp
的值是相同的,其实这是一项编译优化:根据 AMD64 ABI 文档的描述rsp
以下 128 字节的区域被称为 red zone,这是一块被保留的内存, 不会被信号或者中断所修改。于是func()
作为叶子函数就可以在不调整栈指针的情况下,使用这块内存保存临时数据。
危险函数
第一类危险函数——scanf
、gets
等
char buf[10];
scanf("%s", buf); // 没有限制读取长度,存在栈溢出
scanf("%10s", buf); // 限制读取长度为 10,但由于 scanf 函数会在字符串末尾自动添加 "\0",如果输入长度正好为 10,就会溢出
scanf("%9s", buf); // 限制长度为 9,这是安全的
// gets 函数不限制读取长度,很容易构成溢出漏洞
第二类危险函数——strcpy
、strcat
、sprintf
等
int len;
char srcbuf[20];
char destbuf[10];
len = read(0, srcbuf, 19); // 这里限制了可读入长度,是安全的
src[len] = 0;
strcpy(destbuf, srcbuf); // 这里将 srcbuf 拷贝到 destbuf,这时候可能造成溢出
可以用来代替上面的的安全函数:strncpy
、strcat
、snprintf
,因为这些函数都有一个 size
参数用来限制长度
ret2text
-
形成条件:危险函数 + 合适的.text 段代码(一般为后门函数)
ret2shellcode
ret2shellcode,即控制程序执行 shellcode 代码。shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。一般来说,shellcode 需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码。
在栈溢出的基础上,要想执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限。
举例:https://gcc.lv/2021/10/26/20211026_ctf_ret2shellcode/
ret2libc
- 简单来说就是利用已知的 libc 函数,来匹配正确的 libc 版本,从而计算出 system 函数的地址
- 举例:https://gcc.lv/2021/11/12/20211112-ctf-ciscn_2019_c_1/
参考
- 《CTF 竞赛权威指南(pwn 篇)》
- https://ctf-wiki.org/
写在最后
一些东西写得很简略,比如 ret2xxx 部分,虽然是学习笔记而已,大致理解就行,但终究还是因为水平不够才写不出来,希望以后能补充更新